S05-05 JS高级-ES6-Symbol、Set、Map、Proxy、Reflect、Iterator、Generator、ES7+
[TOC]
Symbol
API
- 构造函数
- Symbol():
(description?)
,创建一个 Symbol - 属性
- Symbol.iterator:
() => iterator
,用于定义一个对象如何被迭代,可以让一个对象成为“可迭代”的对象。返回一个迭代器对象。 - Symbol.prototype.description:
string
,(只读),返回 Symbol 对象的可选描述的字符串。 - 方法
- Symbol.for():
(key)
,会根据给定的键key
,来从运行时的 symbol 注册表中找到对应的 symbol,如果找到了,则返回它,否则,新建一个与该键关联的 symbol。 - Symbol.keyFor(sym):``,用来获取全局 symbol 注册表中与某个 symbol 关联的键 key。
- 相关方法
- Object.getOwnPropertySymbols():
(obj)
,返回一个给定对象自身的所有 Symbol 属性的数组。
构造函数
Symbol():
(description?)
,用于创建一个 Symbol对象。主要用于对象的属性名,可以避免属性名冲突。description?:
string
,对 symbol 的描述,可用于调试但不是访问 symbol 本身,不影响符号的唯一性。返回:
sym:
Symbol
,返回一个Symbol对象。- js
// 基本使用 const symbol1 = Symbol(); const symbol2 = Symbol('description'); console.log(symbol1); // Symbol() console.log(symbol2); // Symbol(description) // 作为对象属性名 const mySymbol = Symbol('myUniqueKey'); const obj = { [mySymbol]: 'value' }; console.log(obj[mySymbol]); // 输出:'value'
属性【
- Symbol.iterator:
() => iterator
,用于定义一个对象如何被迭代,可以让一个对象成为“可迭代”的对象。返回一个迭代器对象。 - Symbol.prototype.description:
string
,(只读),返回 Symbol 对象的可选描述的字符串。
方法【
- Symbol.for():
(key)
,会根据给定的键key
,来从运行时的 symbol 注册表中找到对应的 symbol,如果找到了,则返回它,否则,新建一个与该键关联的 symbol。 - Symbol.keyFor(sym):``,用来获取全局 symbol 注册表中与某个 symbol 关联的键 key。
相关方法【
- Object.getOwnPropertySymbols():
(obj)
,返回一个给定对象自身的所有 Symbol 属性的数组。
痛点:
那么为什么需要 Symbol 呢?
- 在 ES6 之前,对象的属性名都是字符串形式,那么很容易造成属性名的冲突;
- 比如原来有一个对象,我们希望在其中添加一个新的属性和值,但是我们在不确定它原来内部有什么内容的情况下,很容易造成冲突,从而覆盖掉它内部的某个属性;
- 比如我们前面在讲 apply、call、bind 实现时,我们有给其中添加一个 fn 属性,那么如果它内部原来已经有了 fn 属性了呢?
- 比如开发中我们使用混入,那么混入中出现了同名的属性,必然有一个会被覆盖掉;
Symbol:
Symbol 是什么呢?Symbol 是 ES6 中新增的一个基本数据类型,翻译为符号。
Symbol 就是为了解决上面的问题,用来生成一个独一无二的值。
- Symbol 值是通过 Symbol() 函数来生成的,生成后可以作为属性名;这是该数据类型仅有的目的
- 也就是在 ES6 中,对象的属性名可以使用字符串,也可以使用 Symbol 值;
特性:
- Symbol 函数执行后每次创建出来的值都是独一无二的。
- 可以在创建 Symbol 值的时候传入一个描述 description(ES10新增特性)。
语法
Symbol(description?)
参数
- description:
string
,对 symbol 的描述,可用于调试但不是访问 symbol 本身
示例
// 1. 通过Symbol函数创建一个Symbol
const s1 = Symbol();
// 2. 创建的时候传入一个description
const s2 = Symbol("s2");
// 3. Symbol函数每次创建出来的值都是独一无二的
console.log(Symbol() == Symbol()); // falses
console.log(Symbol() === Symbol()); // false
// 4. Symbol作为对象属性的标识符
const obj = {
[s1]: "name",
[s2]: "age",
};
console.log(obj); // {Symbol(): 'name', Symbol(s2): 'age'}
// 5. 获取Symbol对应的key
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(), Symbol(s2)]
// 6. Symbol.for(key)
const s3 = Symbol.for("s3");
console.log(s3); // Symbol(s3)
// 7. 相同的key,通过`Symbol.for()`可以生成相同的Symbol值
const s4 = Symbol.for("ss");
const s5 = Symbol.for("ss");
console.log(s4 === s5); // true
// 8. 通过`Symbol.keyFor()` 可以获取通过Symbol.for()传入的key
console.log(Symbol.keyFor(s2)); // undefined
console.log(Symbol.keyFor(s5)); // ss
Symbol 作为属性名
我们通常会使用 Symbol 在对象中表示唯一的属性名
相同值的 Symbol
前面我们讲 Symbol 的目的是为了创建一个独一无二的值,那么如果我们现在就是想创建相同的 Symbol应该怎么来做呢?
- 我们可以使用 Symbol.for 方法来做到这一点
- 并且我们可以通过 Symbol.keyFor 方法来获取对应的 key
相同的 key,通过Symbol.for()
可以生成相同的 Symbol 值
const s4 = Symbol.for("ss");
const s5 = Symbol.for("ss");
console.log(s4 === s5); // true
通过Symbol.keyFor()
可以获取通过 Symbol.for()传入的 key
console.log(Symbol.keyFor(s2)); // undefined
console.log(Symbol.keyFor(s5)); // ss
Set
API
- 属性
- size:``,返回 Set 中元素的个数
- 方法
- add(value):
返回:Set对象
,添加某个元素 - delete(value):
返回:Boolean
,从 set 中删除和这个值相等的元素 - has(value):
返回:Boolean
,判断 set 中是否存在某个元素 - clear():
返回:void
,清空 set 中所有的元素 - forEach(callback, thisArg?):
返回:undefined
,通过 forEach 遍历 set- 参数
- callback:
function(value?, key?, set?)
,为集合中每个元素执行的回调函数 - thisArg:
,在执行
callback
时作为this
使用
注意:Set 支持 for of 的遍历
常见方法
添加元素
// 2. 添加Set - add()
set.add("Tom");
console.log(set); // Set(1) {'Tom'}
// 3. Set中不能放入重复的元素
set.add("Jack");
set.add("Jack");
console.log(set); // Set(2) {'Tom', 'Jack'}
删除元素
// 5. 常见方法 - delete()
console.log(set); // Set(2) {'Tom', 'Jack'}
set.delete("Tom");
console.log(set); // Set(1) {'Jack'}
是否包含某个元素
// 6. 常见方法 - has()
console.log(set.has("Jack")); // true
清空 set
// 7. 常见方法 - clear()
set.clear();
console.log(set); // Set(0) {size: 0}
forEach 遍历
// 8. 常见方法 - forEach()
set2.forEach((item, index, set) => {
console.log(item, index, set); // 刘备 刘备 Set(4) {'刘备', '关羽', '张飞', '吕布'}
});
语法
Set 对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用。
new Set(iterable?)
参数
- iterable:``,如果传递一个可迭代对象,它的所有元素将不重复地被添加到新的 Set 中
返回值
- 一个新的
Set
对象
示例
const mySet = new Set();
mySet.add(1); // Set [ 1 ]
mySet.add(5); // Set [ 1, 5 ]
mySet.add(5); // Set [ 1, 5 ]
mySet.add("some text"); // Set [ 1, 5, 'some text' ]
const o = { a: 1, b: 2 };
mySet.add(o);
基本使用
在 ES6 之前,我们存储数据的结构主要有两种:数组、对象。
- 在 ES6 中新增了另外两种数据结构:Set、Map,以及它们的另外形式 WeakSet、WeakMap。
Set 是一个新增的数据结构,可以用来保存数据,类似于数组,但是和数组的区别是元素不能重复。
- 创建 Set 我们需要通过 Set 构造函数(暂时没有字面量创建的方式):
我们可以发现 Set 中存放的元素是不会重复的,那么 Set 有一个非常常用的功能就是给数组去重。
创建 Set
// 1. 创建Set
const set = new Set();
console.log(set); // Set(0) {size: 0}
2 个空对象不是重复的元素
// 10. 2个空对象不是重复的元素
const set4 = new Set();
set4.add({});
set4.add({});
console.log(set4); // Set(2) {{…}, {…}}
应用:数组去重
// 4. 应用:数组去重
const arr = ["刘备", "关羽", "张飞", "吕布", "关羽", "刘备"];
const set2 = new Set(arr);
console.log(set2); // Set(4) {'刘备', '关羽', '张飞', '吕布'}
const set3 = Array.from(set2);
console.log(set3); // (4) ['刘备', '关羽', '张飞', '吕布']
// 简单写法一
console.log(Array.from(new Set(arr))); // (4) ['刘备', '关羽', '张飞', '吕布']
// 或者写法二
console.log([...new Set(arr)]); // (4) ['刘备', '关羽', '张飞', '吕布']
之前数组去重的做法
set 支持 for...of 遍历
只要是可迭代对象都可以通过 for...of 遍历
// 9. 通过for...of遍历Set
for (const item of set2) {
console.log(item); // 刘备 关羽 张飞 吕布
}
WeakSet
API
- 方法
- add(value):
返回:WeakSet对象
,添加某个元素 - delete(value):
返回:Boolean
,从 WeakSet 中删除和这个值相等的元素 - has(value):
返回:Boolean
,判断 WeakSet 中是否存在某个元素
语法
WeakSet 对象允许你将弱保持对象存储在一个集合中
和Set类似的另外一个数据结构称之为WeakSet,也是内部元素不能重复的数据结构。
语法
const ws = new WeakSet(iterable?)
参数:
- iterable: 可迭代对象
特性:
- WeakSet内部的元素不能重复
基本使用
和 Set 类似的另外一个数据结构称之为 WeakSet,也是内部元素不能重复的数据结构。
那么和 Set有什么区别呢?
- 区别一:WeakSet 中只能存放对象类型,不能存放基本数据类型;
- 区别二:WeakSet对元素的引用是弱引用,如果没有其他引用对某个对象进行引用,那么 GC 可以对该对象进行回收;
WeakSet 中只能存放对象类型
普通对象的内存图
解释:普通对象被重新赋值为 null 时,就断开了和内存中对象的联系,但是由于之前已经将对象的内存地址赋值给了数组 arr,赋值为 null 后这些对象依然被数组 arr 所引用,所以它们并不会被销毁
WeakSet 内存图
解释: 添加到 WeakSet 中的对象都是弱引用,可能会被 GC 随时回收
注意:WeakSet 不能遍历
因为 WeakSet 只是对对象的弱引用,如果我们遍历获取到其中的元素,那么有可能造成对象不能正常的销毁。
所以存储到 WeakSet 中的对象是没办法获取的;
应用:限制类中方法的调用者
- 事实上这个问题并不好回答,我们来使用一个 Stack Overflow 上的答案;
此处用 WeakSet 的好处:想要销毁实例对象 p 的时候,可以直接通过p = null
销毁,如果使用 Set 的话,由于实例对象一直被 Set 引用,所以无法销毁
Map
API
Map常见的属性:
- size:返回Map中元素的个数;
Map常见的方法:
set(key, value):在Map中添加key、value,并且返回整个Map对象;
get(key):根据key获取Map中的value;
has(key):判断是否包括某一个key,返回Boolean类型;
delete(key):根据key删除一个键值对,返回Boolean类型;
clear():清空所有的元素;
forEach(callback, [, thisArg]):通过forEach遍历Map;
Map也可以通过for of进行遍历。
示例:
Map的基本使用
另外一个新增的数据结构是Map,用于存储映射关系。
但是我们可能会想,在之前我们可以使用对象来存储映射关系,他们有什么区别呢?
事实上我们对象存储映射关系只能用字符串(ES6新增了Symbol)作为属性名(key);
某些情况下我们可能希望通过其他类型作为key,比如对象,这个时候会自动将对象转成字符串来作为key;
那么我们就可以使用Map:
语法
const m = new Map(iterable?)
参数:
- iterable:可迭代对象
Map和对象的区别
- 属性名: 对象存储时只能使用字符串和Symbol作为属性名;而Map可以使用任何值(包括对象和基本类型)作为属性名
特性: Map中不能存储包含相同键的元素,可以存储包含相同值得元素
const m4 = new Map([
[1, "tom"],
[1, "jack"],
["name", "jerry"],
["nick", "jerry"],
]);
console.log("m4: ", m4); // => {1 => 'jack', 'name' => 'jerry', 'nick' => 'jerry'}
WeakMap
API
set(key, value):在Map中添加key、value,并且返回整个Map对象;
get(key):根据key获取Map中的value;
has(key):判断是否包括某一个key,返回Boolean类型;
delete(key):根据key删除一个键值对,返回Boolean类型;
WeakMap的使用
和Map类型的另外一个数据结构称之为WeakMap,也是以键值对的形式存在的。
语法:
const wm = new WeakMap(iterable?)
参数:
- iterable:可迭代对象
WeakMap和Map的区别:
区别一:WeakMap的key只能使用对象,不接受其他的类型作为key;
区别二:WeakMap的key对对象的引用是弱引用,如果没有其他引用引用这个对象,那么GC可以回收该对象;
WeakMap的应用
注意:WeakMap也是不能遍历的
- 没有forEach方法,也不支持通过for of的方式进行遍历;
那么我们的WeakMap有什么作用呢?(后续专门讲解)
Proxy▸
API【
监听对象属性的操作
需求: 有一个对象,我们希望监听这个对象中的属性被设置或获取的过程
- 通过我们前面所学的知识,能不能做到这一点呢?
思路: 我们可以通过之前的属性描述符中的存储属性描述符来做到;
上边这段代码就利用了前面讲过的 Object.defineProperty 的存储属性描述符来对属性的操作进行监听。
缺点:
但是这样做有什么缺点呢?
首先,Object.defineProperty的设计初衷,不是为了去监听截止一个对象中所有的属性的。
- 我们在定义某些属性的时候,初衷其实是定义普通的属性,但是后面我们强行将它变成了数据属性描述符。
其次,如果我们想监听更加丰富的操作,比如新增属性、删除属性,那么Object.defineProperty是无能为力的。
所以我们要知道,存储数据描述符设计的初衷并不是为了去监听一个完整的对象。
Proxy基本使用
在ES6中,新增了一个Proxy类,这个类从名字就可以看出来,是用于帮助我们创建一个代理的:
也就是说,如果我们希望监听一个对象的相关操作,那么我们可以先创建一个代理对象(Proxy对象);
之后对该对象的所有操作,都通过代理对象来完成,代理对象可以监听我们想要对原对象进行哪些操作;
我们可以将上面的案例用Proxy来实现一次:
首先,我们需要new Proxy对象,并且传入需要侦听的对象以及一个处理对象,可以称之为handler;
其次,我们之后的操作都是直接对Proxy的操作,而不是原有的对象,因为我们需要在handler里面进行侦听;
语法:
const p= new Proxy(target, handler)
参数:
- target:
,要使用Proxy包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
- handler:
,一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理p的行为
示例: 基本使用
Proxy的set和get捕获器
如果我们想要侦听某些具体的操作,那么就可以在handler中添加对应的捕获器(Trap):
set和get分别对应的是函数类型;
set函数有四个参数:
- target:目标对象(侦听的对象);
- property:将被设置的属性key;
- value:新属性值;
- receiver:调用的代理对象;
get函数有三个参数:
- target:目标对象(侦听的对象);
- property:被获取的属性key;
- receiver:调用的代理对象;
Proxy所有捕获器
13个捕获器分别是做什么的呢?
- 监听普通对象
- handler.get(target, prop, receiver?):
obj.name
,获取属性值 - handler.set(target, prop, newValue, receiver?):
obj.name = 'mr'
,设置属性值 - handler.has(target, prop):
in
, 判断是否存在某属性 - handler.defineProperty(target, prop, descriptor):
Object.defineProperty
, 设置属性描述符 - handler.deleteProperty(target, prop):
delete
,删除属性 - 监听函数对象
- handler.apply(target, thisArg, args):
Object.prototype.apply
,函数调用 - handler.construct(target, args, newTarget?):
new
,调用构造函数 - handler.getPrototypeOf(target):
Object.getPrototypeOf
, 获取对象的原型 - handler.setPrototypeOf(target, prototype):
Object.setPrototypeOf
, 设置对象的原型 - handler.isExtensible(target):
Object.isExtensible
, 判断是否可以新增属性 - handler.preventExtensions(target):
Object.preventExtensions
, 阻止对象扩展 - handler.ownKeys(target):
Object.getOwnPropertyNames
,Object.getOwnPropertySymbols
,获取自身上的所有属性 - handler.getOwnPropertyDescriptor(target, prop):
Object.getOwnPropertyDescriptor
, 获取自身上的属性描述符
示例:
Proxy监听函数对象
当然,我们还会看到捕捉器中还有construct和apply,它们是应用于监听函数对象的:
Reflect▸
API
Reflect中有哪些常见的方法呢?它和Proxy是一一对应的,也是13个:
- 监听普通对象
- Reflect.get(target, prop, receiver?):
obj.name
,获取属性值 - Reflect.set(target, prop, newValue, receiver?):
obj.name = 'mr'
,设置属性值 - Reflect.has(target, prop):
in
, 判断是否存在某属性 - Reflect.defineProperty(target, prop, descriptor):
Object.defineProperty
, 设置属性描述符 - Reflect.deleteProperty(target, prop):
delete
,删除属性 - 监听函数对象
- Reflect.apply(target, thisArg, args):
Object.prototype.apply
,函数调用 - Reflect.construct(target, args, newTarget?):
new
,调用构造函数 - Reflect.getPrototypeOf(target):
Object.getPrototypeOf
, 获取对象的原型 - Reflect.setPrototypeOf(target, prototype):
Object.setPrototypeOf
, 设置对象的原型 - Reflect.isExtensible(target):
Object.isExtensible
, 判断是否可以新增属性 - Reflect.preventExtensions(target):
Object.preventExtensions
, 阻止对象扩展 - Reflect.ownKeys(target):
Object.getOwnPropertyNames
,Object.getOwnPropertySymbols
,获取自身上的所有属性 - Reflect.getOwnPropertyDescriptor(target, prop):
Object.getOwnPropertyDescriptor
, 获取自身上的属性描述符
Reflect的作用
Reflect也是ES6新增的一个API,它是一个对象,字面的意思是反射。
作用:
那么这个Reflect有什么用呢?
它主要提供了很多操作JavaScript对象的方法,有点像Object中操作对象的方法;
比如Reflect.getPrototypeOf(target)类似于 Object.getPrototypeOf();
比如Reflect.defineProperty(target, propertyKey, attributes)类似于Object.defineProperty() ;
如果我们有Object可以做这些操作,那么为什么还需要有Reflect这样的新增对象呢?
这是因为在早期的ECMA规范中没有考虑到这种对 对象本身 的操作如何设计会更加规范,所以将这些API放到了Object上面;
但是Object作为一个构造函数,这些操作实际上放到它身上并不合适;
另外还包含一些类似于 in、delete操作符,让JS看起来是会有一些奇怪的;
所以在ES6中新增了Reflect,让我们这些操作都集中到了Reflect对象上;
另外在使用Proxy时,可以做到不操作原对象;
那么Object和Reflect对象之间的API关系,可以参考MDN文档:
Reflect的使用
那么我们可以将之前Proxy案例中对原对象的操作,都修改为Reflect来操作:
优点: 使用Reflect结合Proxy代理对象的优点
- 优点一:代理对象的目的:实现不再直接操作原对象
- 优点二:Reflect.set方法有返回布尔值,可以判断本次操作是否成功
- 优点三:receiver就是外层Proxy对象。Reflect.set/get的最后一个参数receiver可以决定对象访问器settter/getter的this指向
Receiver的作用
我们发现在使用getter、setter的时候有一个receiver的参数,它的作用是什么呢?
- 如果我们的源对象(obj)有setter、getter的访问器属性,那么可以通过receiver来改变里面的this;
我们来看这样的一个对象:
Reflect的construct
Iterator▸
API
iterator:({next(), return()?})
,JS中的迭代器,实现了next方法的对象,在next方法中必须返回一个包含了done和value属性的对象
next():
(arg?)
,返回一个包含value
和done
属性的对象。- arg?:``,可选参数。
- 返回:
{done, value}
- done:
boolean
,是否已经遍历完所有元素。false
:表示迭代未完成,value值为any;true
:表示迭代完毕,value值为undefined
- value:
any | undefined
,表示当前元素的值。
return()?:
(value)
,结束当前的迭代,并返回一个包含 done 属性的对象。- value:
any
,是return()
方法返回的值,可以是任何类型的值。通常用来返回一个终止值,标识迭代的结束。 - 返回:
{done, value?}
- done:
boolean
,表示迭代是否完成。调用return()
时,这个属性的值通常是true
。 - value?:
any
,返回迭代结束时的值。可以用来返回终止时需要返回的值。
- value:
- js
const customIterator = { items: [1, 2, 3, 4], index: 0, next() { if (this.index < this.items.length) { return { value: this.items[this.index++], done: false }; } else { return { value: undefined, done: true }; } }, return(value) { console.log("Iteration is being stopped early!"); return { value: value, done: true }; } }; const iter = customIterator; console.log(iter.next()); // { value: 1, done: false } console.log(iter.next()); // { value: 2, done: false } console.log(iter.return(99)); // Iteration is being stopped early! { value: 99, done: true }
概念:
- 迭代器:Iterator。它是一种提供遍历集合元素的机制。
- 可迭代对象:Iterable。它是一种实现了Iterator Protocol方法的对象。
- 迭代器协议:Iterator Protocol。它是一种定义迭代器对象的标准,它规定了迭代器对象必须实现的方法和属性。
迭代器
介绍
迭代器(iterator),帮助用户在容器对象(container,例如链表或数组)上遍访的对象,使用该接口无需关心对象的内部实现细节。
其行为像数据库中的光标,迭代器最早出现在1974年设计的CLU编程语言中;
在各种编程语言的实现中,迭代器的实现方式各不相同,但是基本都有迭代器,比如Java、Python等;
从迭代器的定义我们可以看出来,迭代器是帮助我们对某个数据结构进行遍历的对象。
在JavaScript中,迭代器也是一个具体的对象,这个对象需要符合迭代器协议(iterator protocol)
迭代器协议定义了产生一系列值(无论是有限还是无限个)的标准方式;在JavaScript中这个标准就是一个特定的next方法;
next方法有如下的要求:
一个无参数或者一个参数的函数,返回一个应当拥有以下两个属性的对象:
done(boolean)
- 如果迭代器可以产生序列中的下一个值,则为 false。(这等价于没有指定 done 这个属性。)
- 如果迭代器已将序列迭代完毕,则为 true。这种情况下,value 是可选的,如果它依然存在,即为迭代结束之后的默认返回值。
value
- 迭代器返回的任何 JavaScript 值。done 为 true 时可省略。
语法
JS中的迭代器:实现了next方法的对象,在next方法中必须返回一个包含了done和value属性的对象
const arr = ['a', 'b', 'c']
let index = 0
const iterator = {
next: function() {
if(index < arr.length) {
return {done: false, vlaue: arr[index++]}
} else {
return {done: true, value: undefined}
}
}
}
示例: 迭代器的代码练习
1、迭代器-基本案例
2、迭代器-封装一个通用的迭代器生成函数
可迭代对象
介绍
但是上面的代码整体来说看起来是有点奇怪的:
我们获取一个数组的时候,需要自己创建一个index变量,再创建一个所谓的迭代器对象;
事实上我们可以对上面的代码进行进一步的封装,让其变成一个可迭代对象;
什么又是可迭代对象呢?
它和迭代器是不同的概念;
当一个对象实现了iterable protocol协议时,它就是一个可迭代对象;
这个对象的要求是必须实现 @@iterator() 方法,在代码中我们使用 Symbol.iterator 访问该属性;
当然我们要问一个问题,我们转成这样的一个东西有什么好处呢?
当一个对象变成一个可迭代对象的时候,就可以进行某些迭代操作;
比如 for...of 操作时,其实就会调用它的 @@iterator 方法;
特性:
1、可迭代对象内部需要实现[Symbol.iterator]
方法,该方法返回一个迭代器。
2、可迭代对象可以进行for...of
遍历
优化
1、迭代infos中的friends属性
next函数用箭头函数书写,可以让其内部的this指向可迭代对象,从而实现更通用的封装
2、迭代对象中的键,值,键值对
内置可迭代对象
事实上我们平时创建的很多原生对象已经实现了可迭代协议,会生成一个可迭代对象的:
- String、Array、Map、Set、arguments对象、NodeList集合;
用法:
▸ 数组: arr[Symbol.iterator]()
▸ Set: set[Symbol.iterator]()
▸ arguments: arguments[Symbol.iterator]()
应用
那么这些东西可以被用在哪里呢?
JS中语法:
for ...of
:遍历`...:展开语法(spread syntax)
yield*
(后面讲)[name, age] = arr
:解构赋值(Destructuring_assignment)
创建一些对象时,可传入可迭代对象:
- new Map():
(iterable?)
,创建Map对象 - new WeakMap():
(iterable?)
,创建WeakMap对象 - new Set():
(iterable?)
,创建Set对象 - new WeakSet():
(iterable?)
,创建WeakSet对象
- new Map():
一些方法的调用:
- Promise.all():
(iterable)
, - Promise.race():
(iterable)
, - Array.from():
(iterable)
,
- Promise.all():
自定义类的迭代
在前面我们看到Array、Set、String、Map等类创建出来的对象都是可迭代对象:
在面向对象开发中,我们可以通过class定义一个自己的类,这个类可以创建很多的对象:
如果我们也希望自定义类创建出来的对象默认是可迭代对象,那么在设计类的时候我们就可以添加上 @@iterator 方法;
案例:创建一个classroom的类
教室中有自己的位置、名称、当前教室的学生;
这个教室可以进来新学生(push);
创建的教室对象是可迭代对象;
自定义类的迭代实现: 自定义类创建出来的对象默认是可迭代对象
迭代器-监听中断
iterator:({next(), return()?})
,JS中的迭代器,实现了next方法的对象,在next方法中必须返回一个包含了done和value属性的对象
next():
(arg?)
,返回一个包含value
和done
属性的对象。- arg?:``,可选参数。
- 返回:
{done, value}
- done:
boolean
,是否已经遍历完所有元素。false
:表示迭代未完成,value值为any;true
:表示迭代完毕,value值为undefined
- value:
any | undefined
,表示当前元素的值。
return()?:
(value)
,结束当前的迭代,并返回一个包含 done 属性的对象。- value:
any
,是return()
方法返回的值,可以是任何类型的值。通常用来返回一个终止值,标识迭代的结束。 - 返回:
{done, value?}
- done:
boolean
,表示迭代是否完成。调用return()
时,这个属性的值通常是true
。 - value?:
any
,返回迭代结束时的值。可以用来返回终止时需要返回的值。
- value:
- js
const customIterator = { items: [1, 2, 3, 4], index: 0, next() { if (this.index < this.items.length) { return { value: this.items[this.index++], done: false }; } else { return { value: undefined, done: true }; } }, return(value) { console.log("Iteration is being stopped early!"); return { value: value, done: true }; } }; const iter = customIterator; console.log(iter.next()); // { value: 1, done: false } console.log(iter.next()); // { value: 2, done: false } console.log(iter.return(99)); // Iteration is being stopped early! { value: 99, done: true }
迭代器在某些情况下会在没有完全迭代的情况下中断:
比如遍历的过程中通过break、return、throw中断了循环操作;
比如在解构的时候,没有解构所有的值;
那么这个时候我们想要监听中断的话,可以添加 return()方法:
Generator▸
概念
生成器: 是ES6中新增的一种函数控制、使用的方案,它可以让我们更加灵活的控制函数什么时候继续执行、暂停执行等。
- 平时我们会编写很多的函数,这些函数终止的条件通常是返回值或者发生了异常。
生成器函数: 也是一个函数,但是和普通的函数有一些区别:
首先,生成器函数需要在function的后面加一个符号:* :
function* foo()
或者function *foo()
其次,生成器函数可以通过yield关键字来控制函数的执行流程:
最后,生成器函数执行时返回一个Generator(生成器):
- 要想执行函数内部的代码,需要通过生成器对象调用它的next方法
- 当遇到yield时,就会中断执行,需要再次调用next方法,才会继续执行
生成器事实上是一种特殊的迭代器;
- MDN:Instead, they return a special type of iterator, called a Generator.
生成器函数
我们发现下面的生成器函数foo的执行体压根没有执行,它只是返回了一个生成器对象。
那么我们如何可以让它执行函数中的东西呢?调用next即可;
我们之前学习迭代器时,知道迭代器的next是会有返回值的;
但是我们很多时候不希望next返回的是一个undefined,这个时候我们可以通过yield来返回结果;
语法
生成器函数是 JavaScript 中的一种特殊函数类型,它通过 function*
语法定义,具有多次返回值的能力,可以暂停和恢复执行。
语法:
function* generatorFunction() {
// 生成器逻辑
}
function*
:生成器函数使用function*
来定义。可以在执行过程中暂停,并且可以多次返回值。- 生成器函数默认在执行时,返回一个生成器对象。
yield
:表示生成器函数在此暂停,并返回一个值。直到外部调用next()
方法。next()
:生成器函数返回的是一个生成器对象,该对象具有next()
方法。调用next()
方法时,生成器从上次暂停的地方继续执行,直到遇到下一个yield
或结束。return()
:可以提前终止生成器并返回一个值。如果在yield
后调用return()
,生成器将不再返回任何值,done
会变为true
。yield*
:是一个委托表达式,用于将生成器的执行委托给另一个生成器。它会一次性执行另一个生成器,返回它的所有值。
用法:
▸ 基本使用
// 1. 定义一个生成器函数
function* foo() {
console.log('1111')
console.log('2222')
yield
console.log('3333')
console.log('4444')
yield
console.log('5555')
console.log('6666')
}
// 2. 调用生成器函数,返回一个生成器对象
const gen = foo();
// 3. 调用生成器对象的next方法
console.log(gen.next()); // 1111, 2222
console.log(gen.next()); // 3333, 4444
console.log(gen.next()); // 5555, 6666
说明:生成器函数的执行流程:
- function 后面会跟上符号
*
- 代码的执行可以被
yeild
控制 - 生成器函数默认子在执行时,返回一个生成器对象
- 要想执行函数内部的代码,需要生成器对象调用它的next()方法
- 当遇到yeild时,就会中断执行
返回值 yield
通过yield返回结果
传递参数 next()
函数既然可以暂停来分段执行,那么函数应该是可以传递参数的,我们是否可以给每个分段来传递参数呢?
答案是可以的;
我们在调用next函数的时候,可以给它传递参数,那么这个参数会作为上一个yield语句的返回值;
注意:也就是说我们是为本次的函数代码块执行提供了一个值;
传递参数规则:
- 第一个yield之前代码块中的参数
name1
是通过foo('next1')
函数传递参数,并通过foo(name1)
接收参数 - 第二个yield之前代码块中的参数
name2
是在调用第二个next('next2')
时传递参数,并通过const name2 = yield 'aaaa'
中的name2接收参数 - 之后的yield代码块中的参数按照第二个yield的参数传递规则依次传递。
- 注意: 第一次调用
next()
时,不传递参数
提前结束 return()
还有一个可以给生成器函数传递参数的方法是通过return函数:
return传值后这个生成器函数就会立即结束,之后调用next不会继续生成值了;
抛出异常 throw()
除了给生成器函数内部传递参数之外,也可以向生成器函数内部抛出异常:
1、通过 generator.throw(new Error('error message'))
向生成器函数内部抛出异常
2、在生成器函数内部捕获异常
注意: 在catch语句中不能继续yield新的值了,但是可以在catch语句外使用yield继续中断函数的执行;
应用
生成器替代迭代器
我们发现生成器是一种特殊的迭代器,那么在某些情况下我们可以使用生成器来替代迭代器:
▸ 利用生成器对之前的迭代器代码进行重构
1、之前的迭代器
2、利用生成器重构之前的迭代器
▸ 生成器函数,可以生成某个范围的值
yield*
yield*
是一个委托表达式,用于将生成器的执行委托给另一个生成器或可迭代对象。它会一次性执行另一个生成器,返回它的所有值。
语法:
yield* iterable
iterable
:可以是任何具有迭代协议的对象,通常是另一个生成器、数组、字符串、Set、Map 等。
特性:
yield*
是yield
语句的一种语法糖。它会依次迭代这个可迭代对象,每次迭代其中的一个值。yield*
只能存在于生成器函数中。yield*
语法非常适合处理递归生成器或将多个生成器组合成一个生成器。
用法:
▸ 基本用法:委托给另一个生成器
function* inner() {
yield 1;
yield 2;
}
function* outer() {
yield* inner(); // 委托给 inner 生成器
yield 3;
}
const gen = outer();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }
▸ 一行代码重构迭代器:委托给数组
▸ 使用 yield* 处理递归
// flatten 是一个递归生成器函数,使用 yield* 将内部的数组元素“平铺”到外部生成器中。
function* flatten(arr) {
for (const item of arr) {
if (Array.isArray(item)) {
yield* flatten(item); // 递归地展开数组
} else {
yield item;
}
}
}
const gen = flatten([1, [2, [3, 4], 5], 6]);
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: 4, done: false }
console.log(gen.next()); // { value: 5, done: false }
console.log(gen.next()); // { value: 6, done: false }
console.log(gen.next()); // { value: undefined, done: true }
▸ 重构自定义类的迭代
在之前的 自定义类的迭代 中,我们也可以换成生成器:
注意: 此处*[Symbol.iterator]()
表示生成器函数,yield*
只能存在于生成器函数中。
对生成器的操作
既然生成器是一个迭代器,那么我们可以对其进行如下的操作:
异步处理▸
学完了我们前面的Promise、生成器等,我们目前来看一下异步代码的最终处理方案。
异步处理方案:
- 方案一:回调嵌套,会造成回调地狱
- 方案二:Promise链式调用
- 方案三:Generator方案
- 方案四:async await方案(终极方案)
案例需求:
我们需要向服务器发送网络请求获取数据,一共需要发送三次请求;
第二次的请求url依赖于第一次的结果;
第三次的请求url依赖于第二次的结果;
依次类推;
▸ 方案一:回调嵌套,会造成回调地狱
▸ 方案二:Promise链式调用
▸ 方案三:Generator方案
▸ 方案四:async await方案(终极方案)
Generator方案
但是上面的代码其实看起来也是阅读性比较差的,有没有办法可以继续来对上面的代码进行优化呢?
自动执行generator函数
目前我们的写法有两个问题:
第一,我们不能确定到底需要调用几层的Promise关系;
第二,如果还有其他需要这样执行的函数,我们应该如何操作呢?
所以,我们可以封装一个工具函数execGenerator自动执行生成器函数:
async await▸
异步函数 async function
async关键字用于声明一个异步函数:
async是asynchronous单词的缩写,异步、非同步;
sync是synchronous单词的缩写,同步、同时;
async异步函数可以有很多中写法:
特性:
1、异步函数默认情况下和普通函数一样
2、异步函数返回的是一个Promise
异步函数的执行流程
异步函数的内部代码执行过程和普通的函数是一致的,默认情况下也是会被同步执行。
异步函数有返回值时,和普通函数会有区别:
情况一:异步函数也可以有返回值,但是异步函数的返回值相当于被包裹到Promise.resolve中;
情况二:如果我们的异步函数的返回值是Promise,状态由会由Promise决定;
情况三:如果我们的异步函数的返回值是一个对象并且实现了thenable,那么会由对象的then方法来决定;
示例: 返回一个普通的值
示例: 返回一个Promise
示例: 返回一个thenable
什么情况下异步函数的结果是rejected:
在异步函数中返回一个执行reject的Promise
如果我们在async中抛出了异常,那么程序它并不会像普通函数一样报错,而是会作为Promise的reject来传递;
示例: 在异步函数中返回一个执行reject的Promise
示例: 在异步函数中抛出了异常
await关键字
async函数另外一个特殊之处就是可以在它内部使用await关键字,而普通函数中是不可以的。
await关键字有什么特点呢?
通常使用await是后面会跟上一个表达式,这个表达式会返回一个Promise;
那么await会等到Promise的状态变成fulfilled状态,之后继续执行异步函数;
如果await后面是一个普通的值,那么会直接返回这个值;
如果await后面是一个thenable的对象,那么会根据对象的then方法调用来决定后续的值;
如果await后面的表达式,返回的Promise是reject的状态,那么会将这个reject结果直接作为函数的Promise的reject值;
示例: await处理异步请求
在异步函数中抛出异常的处理方式
1、方式一:在异步函数后的catch中捕获异常
2、方式二:在异步函数内部,通过try...catch,捕获异常
ES7
ES7 - includes
在ES7之前,如果我们想判断一个数组中是否包含某个元素,需要通过 indexOf 获取结果,并且判断是否为 -1。
在ES7中,我们可以通过includes来判断一个数组中是否包含一个指定的元素,根据情况,如果包含则返回 true,否则返回false。
ES7 –指数运算符
在ES7之前,计算数字的乘方需要通过 Math.pow 方法来完成。
在ES7中,增加了 ** 运算符,可以对数字来计算乘方。
ES8
Object.values
之前我们可以通过 Object.keys 获取一个对象所有的key
在ES8中提供了 Object.values 来获取所有的value值:
Object.entries
通过 Object.entries 可以获取到一个数组,数组中会存放可枚举属性的键值对数组。
- 可以针对对象、数组、字符串进行操作;
padStart,padEnd
某些字符串我们需要对其进行前后的填充,来实现某种格式化效果,ES8中增加了 padStart 和 padEnd 方法,分别是对字符串的首尾进行填充的。
应用: 对身份证、银行卡的前面位数进行隐藏:
尾部逗号
在ES8中,我们允许在函数定义和调用时多加一个逗号:
Object Descriptors
Object.getOwnPropertyDescriptors
- 这个在之前已经讲过了,这里不再重复。
Async Function:async、await
- 后续讲完Promise讲解
ES9
ES9新增知识点
Async iterators:后续迭代器讲解
Object spread operators:前面讲过了
Promise finally:后续讲Promise讲解
ES10
flat、flatMap
flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。
flatMap() 方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组。
注意一:flatMap是先进行map操作,再做flat的操作;
注意二:flatMap中的flat相当于深度为1;
Object.fromEntries
在前面,我们可以通过 Object.entries 将一个对象转换成 entries
那么如果我们有一个entries了,如何将其转换成对象呢?
- ES10提供了 Object.formEntries来完成转换:
应用: 那么这个方法有什么应用场景呢?
trimStart、trimEnd
去除一个字符串首尾的空格,我们可以通过trim方法,如果单独去除前面或者后面呢?
- ES10中给我们提供了trimStart和trimEnd;
ES10 其他知识点
Symbol description:已经讲过了
Optional catch binding:后面讲解try cach讲解
ES11
BigInt
在早期的JavaScript中,我们不能正确的表示过大的数字:
- 大于MAX_SAFE_INTEGER的数值,表示的可能是不正确的。
那么ES11中,引入了新的数据类型BigInt,用于表示大整数:
- BigInt的表示方法是在数值的后面加上n
空值合并运算符
ES11,Nullish Coalescing Operator增加了空值合并运算符(??):
当foo是undefined或null时,取默认值
可选链
可选链(?.)也是ES11中新增一个特性,主要作用是让我们的代码在进行null和undefined判断时更加清晰和简洁:
语法
obj.val?.prop // 示例:obj.friend?.name
obj.val?.[expr] // 示例:obj.friends?.[0]
obj.func?.(args) // 示例:obj.friend?.running?.()
globalThis
在之前我们希望获取JavaScript环境的全局对象,不同的环境获取的方式是不一样的
比如在浏览器中可以通过this、window来获取;
比如在Node中我们需要通过global来获取;
在ES11中对获取全局对象进行了统一的规范:globalThis
for..in标准化
在ES11之前,虽然很多浏览器支持for...in来遍历对象类型,但是并没有被ECMA标准化。
在ES11中,对其进行了ECMA标准化,for...in是用于遍历对象的key的:
ES11其他知识点
Dynamic Import:后续ES Module模块化中讲解。
Promise.allSettled:后续讲Promise的时候讲解。
import meta:后续ES Module模块化中讲解。
ES12
FinalizationRegistry
FinalizationRegistry 对象可以让你在对象被垃圾回收时请求一个回调。
FinalizationRegistry 提供了这样的一种方法:当一个在注册表中注册的对象被回收时,请求在某个时间点上调用一个清理回调。(清理回调有时被称为 finalizer );
你可以通过调用register方法,注册任何你想要清理回调的对象,传入该对象和所含的值;
WeakRef
如果我们默认将一个对象赋值给另外一个引用,那么这个引用是一个强引用:
如果我们希望是一个弱引用的话,可以使用WeakRef;
逻辑赋值运算符
x &&= y
:逻辑与赋值。仅在x
为真值时为其赋值x ||= y
:逻辑或赋值。仅在x
为假值时为其赋值x ??= y
:逻辑空赋值。仅在x
为*空值(null或undefined)*时为其赋值
ES12其他知识点
Numeric Separator:讲过了;
String.prototype.replaceAll:字符串替换;
ES13
at()
前面我们有学过字符串、数组的at方法,它们是作为ES13中的新特性加入的:
Object.hasOwn()
Object中新增了一个静态方法(类方法): Object.hasOwn(obj, propKey)
- 该方法用于判断一个对象中是否有某个自己的属性;
Object.hasOwn和Object.prototype.hasOwnProperty的区别:
- 区别一:防止对象内部有重写hasOwnProperty
- 区别二:对于隐式原型指向null的对象, hasOwnProperty无法进行判断
类中的新成员
在ES13中,新增了定义class类中成员字段(field)的其他方式:
- 实例属性:public / private
- 类属性(静态属性):public / private
- 静态代码块:先执行静态代码块,再执行构造方法